import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class PBKDF2Engine {
    protected PBKDF2Parameters parameters;

    protected MacBasedPRF prf;

    public PBKDF2Engine() {
        this.parameters = null;
        prf = null;
    }

    public PBKDF2Engine(PBKDF2Parameters parameters) {
        this.parameters = parameters;
        prf = null;
    }

    public PBKDF2Engine(PBKDF2Parameters parameters, MacBasedPRF prf) {
        this.parameters = parameters;
        this.prf = prf;
    }

    public byte[] deriveKey(String inputPassword) {
        return deriveKey(inputPassword, 0);
    }

    public byte[] deriveKey(String inputPassword, int dkLen) {
        byte[] r = null;
        byte P[] = null;
        String charset = parameters.getHashCharset();
        if (inputPassword == null) {
            inputPassword = "";
        }
        try {
            if (charset == null) {
                P = inputPassword.getBytes();
            } else {
                P = inputPassword.getBytes(charset);
            }
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        assertPRF(P);
        if (dkLen == 0) {
            dkLen = prf.getHLen();
        }
        r = PBKDF2(prf, parameters.getSalt(), parameters.getIterationCount(), dkLen);
        return r;
    }

    protected void assertPRF(byte[] P) {
        if (prf == null) {
            prf = new MacBasedPRF(parameters.getHashAlgorithm());
        }
        prf.init(P);
    }

    public MacBasedPRF getPseudoRandomFunction() {
        return prf;
    }

    public void setPseudoRandomFunction(MacBasedPRF prf) {
        this.prf = prf;
    }

    public PBKDF2Parameters getParameters() {
        return parameters;
    }

    public void setParameters(PBKDF2Parameters parameters) {
        this.parameters = parameters;
    }

    protected byte[] PBKDF2(MacBasedPRF prf, byte[] S, int c, int dkLen) {
        if (S == null) {
            S = new byte[0];
        }
        int hLen = prf.getHLen();
        int l = ceil(dkLen, hLen);
        int r = dkLen - (l - 1) * hLen;
        byte T[] = new byte[l * hLen];
        int ti_offset = 0;
        for (int i = 1; i <= l; i++) {
            _F(T, ti_offset, prf, S, c, i);
            ti_offset += hLen;
        }
        if (r < hLen) {
            // Incomplete last block
            byte DK[] = new byte[dkLen];
            System.arraycopy(T, 0, DK, 0, dkLen);
            return DK;
        }
        return T;
    }

    protected int ceil(int a, int b) {
        int m = 0;
        if (a % b > 0) {
            m = 1;
        }
        return a / b + m;
    }

    protected void _F(byte[] dest, int offset, MacBasedPRF prf, byte[] S, int c, int blockIndex) {
        int hLen = prf.getHLen();
        byte U_r[] = new byte[hLen];

        // U0 = S || INT (i);
        byte U_i[] = new byte[S.length + 4];
        System.arraycopy(S, 0, U_i, 0, S.length);
        INT(U_i, S.length, blockIndex);

        for (int i = 0; i < c; i++) {
            U_i = prf.doFinal(U_i);
            xor(U_r, U_i);
        }
        System.arraycopy(U_r, 0, dest, offset, hLen);
    }

    protected void xor(byte[] dest, byte[] src) {
        for (int i = 0; i < dest.length; i++) {
            dest[i] ^= src[i];
        }
    }

    protected void INT(byte[] dest, int offset, int i) {
        dest[offset + 0] = (byte) (i / (256 * 256 * 256));
        dest[offset + 1] = (byte) (i / (256 * 256));
        dest[offset + 2] = (byte) (i / (256));
        dest[offset + 3] = (byte) (i);
    }

    public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
        String password = "password";
        String candidate = null;
        PBKDF2HexFormatter formatter = new PBKDF2HexFormatter();
        int iterations = 1000;

        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
        byte[] salt = new byte[8];
        sr.nextBytes(salt);
        PBKDF2Parameters p = new PBKDF2Parameters("HmacSHA256", "ISO-8859-1", salt, iterations);
        PBKDF2Engine e = new PBKDF2Engine(p);
        p.setDerivedKey(e.deriveKey(password));
        candidate = formatter.toString(p);
        System.out.println(candidate);
    }
}
